package com.simpou.commons.utils.validation;

import com.simpou.commons.utils.behavior.Condition;
import com.simpou.commons.utils.behavior.Validatable;
import com.simpou.commons.utils.behavior.Validator;
import com.simpou.commons.utils.exception.model.RuleViolation;
import com.simpou.commons.utils.lang.Numbers;
import com.simpou.commons.utils.string.RegexHelper;
import com.simpou.commons.utils.string.Strings;

import java.lang.reflect.Modifier;

import java.util.Collection;

/**
 * Classe utilitária para realização de validações diversas sobre objetos e
 * tipos primitivos. Use em métodos ou construtores para validar parâmetros
 * passados passados a eles.
 *
 * @author Jonas Pereira
 * @since 2012-06-12
 * @version 2012-10-22
 */
public class Assertions {

    static final String PARAM_MSG = "Validation error on parameter \"{0}\". ";

    public static final String MSG_INVALID_SIZE = "Invalid size. Given {0}, required {1}.";

    public static final String MSG_INVALID = "Invalid object. Cause: {0}";

    public static final String MSG_NOT_NULL = "Not null required.";

    public static final String MSG_NULL = "Null required.";

    public static final String MSG_NOT_EMPTY = "Not null and not empty required.";

    public static final String MSG_NOT_IN_RANGE = "Number must be between {0} and {1} but was {2}.";

    public static final String MSG_LESS_THAN = "Number must be greater than or equal {0} but was {1}.";

    public static final String MSG_GREATER_THAN = "Number must be lower than or equal {0} but was {1}.";

    public static final String MSG_NOT_INSTANCE_OF = "Invalid instance. Given {0}, required {1}.";

    public static final String MSG_NOT_MATCHES = "Value {0} not matches regex {1}.";

    public static final String MSG_NOT_AN_INTERFACE = "An interface required.";

    public static final String MSG_CHECK_FAIL = "Object not agree with conditions \"{0}\".";

    /**
     * @param regex Expressão regex.
     * @param value Valor a ser validado.
     * @throws java.lang.IllegalArgumentException Se valor não for válido
     * segundo a expressão.
     */
    public static String matches(final String regex, final String value) {
        return matches(regex, value, MSG_NOT_MATCHES, value, regex);
    }

    /**
     * @param regex Expressão regex.
     * @param value Valor a ser validado.
     * @param message Mensagem a ser exibida em caso de erro de validação.
     * @param params Parâmetros da mensagem na forma: {0},{1}...{n}.
     * @throws java.lang.IllegalArgumentException Se valor não for válido
     * segundo a expressão.
     */
    public static String matches(final String regex, final String value,
            final String message, final Object... params) {
        boolean matches = RegexHelper.matches(regex, value);

        if (matches) {
            return value;
        } else {
            throwNewIllegalArgument(value, message, params);

            return null;
        }
    }

    /**
     * Use para validações de parâmetros em métodos ou construtores. Monta uma
     * mensagem de erro contendo o nome do parâmetro que sofreu erro de
     * validação juntamente com o motivo do erro. O texto contendo o nome do
     * parâmetro é padrão.
     *
     * @param invalidMessage Motivo pelo qual o parâmetro é inválido. Podem ser
     * usadas as mensagens estáticas desta classe (MSG_*).
     * @param paramName Nome do parâmetro que não passou na validação.
     * @return Mensagem de erro informando o nome do parâmetro inválido e o
     * motivo do erro.
     */
    public static String getParamMessage(final String invalidMessage,
            final String paramName) {
        return getParamMessage(PARAM_MSG, invalidMessage, paramName);
    }

    /**
     * Use para validações de parâmetros em métodos ou construtores. Monta uma
     * mensagem de erro contendo o nome do parâmetro que sofreu erro de
     * validação juntamente com o motivo do erro.
     *
     * @param customMsgPreffix Texto usado para descrever o nome do parâmetro.
     * Deve conter o padrão de substituição: {0}.
     * @param invalidMessage Motivo pelo qual o parâmetro é inválido. Podem ser
     * usadas as mensagens estáticas desta classe (MSG_*).
     * @param paramName Nome do parâmetro que não passou na validação.
     * @return Mensagem de erro informando o nome do parâmetro inválido e o
     * motivo do erro.
     * @throws IllegalArgumentException Se prefixo não contiver padrão de
     * substituição.
     */
    public static String getParamMessage(final String customMsgPreffix,
            final String invalidMessage, final String paramName) {
        if (!customMsgPreffix.contains("{0}")) {
            throw new IllegalArgumentException(
                    "Custom message must contains \"{0}\".s");
        }

        return Strings.replaceParams(PARAM_MSG, paramName)
                + invalidMessage;
    }

    /**
     * Valida se vários objetos são não nulos.
     *
     * @param objects Objetos que devem ser não nulos.
     * @throws java.lang.NullPointerException Se algum objeto for null.
     */
    public static void notNull(final Object... objects) {
        notNull(MSG_NOT_NULL, objects);
    }

    /**
     * Valida se vários objetos são não nulos.
     *
     * @param message Mensagem a ser exibida em caso de erro de validação.
     * @param objects Objetos que devem ser não nulos.
     * @throws java.lang.NullPointerException Se algum objeto for null.
     */
    public static void notNull(final String message, final Object... objects) {
        for (final Object object : objects) {
            notNull(object, message);
        }
    }

    /**
     * Valida se uma array é não nulo.
     *
     * @param clasz Tipo do array.
     * @param array Array.
     * @return Array se não for nulo.
     * @throws NullPointerException Se array nulo.
     */
    public static <T> T[] notNull(final Class<T> clasz, final T[] array) {
        return notNull(array, MSG_NOT_NULL);
    }

    /**
     * Valida se uma array é não nulo.
     *
     * @param array Array a ser validado.
     * @param message Mensagem a ser exibida em caso de erro de validação.
     * @return Array sem nenhuma alteração.
     * @throws NullPointerException Se array nulo.
     */
    public static <T> T[] notNull(final T[] array, final String message) {
        notNull(message, array);

        return array;
    }

    /**
     * Valida se um objeto é não nulo.
     *
     * @param t a T object.
     * @return Objeto validado sem nenhuma alteração.
     * @throws java.lang.NullPointerException Se objeto for null.
     */
    public static <T> T notNull(final T t) {
        return notNull(t, MSG_NOT_NULL);
    }

    /**
     * Valida se um objeto é nulo.
     *
     * @param t a T object.
     * @return Objeto validado sem nenhuma alteração.
     * @throws java.lang.IllegalArgumentException Se objeto for não null.
     */
    public static <T> T isNull(final T t) {
        return isNull(t, MSG_NULL);
    }

    /**
     * Valida se um objeto é não nulo.
     *
     * @param t a T object.
     * @param message Mensagem a ser exibida em caso de erro de validação.
     * @return Objeto validado sem nenhuma alteração.
     * @throws java.lang.NullPointerException Se objeto for null.
     */
    public static <T> T notNull(final T t, final String message) {
        if (t == null) {
            throw new NullPointerException(getErrorMsg(null, message));
        } else {
            return t;
        }
    }

    /**
     * Valida se um objeto é nulo.
     *
     * @param t a T object.
     * @param message Mensagem a ser exibida em caso de erro de validação.
     * @return Objeto validado sem nenhuma alteração.
     * @throws java.lang.IllegalArgumentException Se objeto for não null.
     */
    public static <T> T isNull(final T t, final String message) {
        if (t != null) {
            throw new IllegalArgumentException(getErrorMsg(t, message));
        } else {
            return t;
        }
    }

    /**
     * Valida se vários objetos são nulos.
     *
     * @param objects Objetos que devem ser nulos.
     * @throws java.lang.IllegalArgumentException Se algum objeto não for null.
     */
    public static void isNull(final Object... objects) {
        isNull(MSG_NULL, objects);
    }

    /**
     * Valida se vários objetos são nulos.
     *
     * @param message Mensagem a ser exibida em caso de erro de validação.
     * @param objects Objetos que devem ser nulos.
     * @throws java.lang.IllegalArgumentException Se algum objeto for não null.
     */
    public static void isNull(final String message, final Object... objects) {
        for (final Object object : objects) {
            isNull(object, message);
        }
    }

    /**
     * Valida se uma string é não nula e não vazia.
     *
     * @param string a {@link java.lang.String} object.
     * @return String validada sem nenhuma alteração.
     * @throws java.lang.NullPointerException Se string for null.
     * @throws java.lang.IllegalArgumentException Se string for vazia.
     */
    public static String notEmpty(final String string) {
        return notEmpty(string, MSG_NOT_EMPTY);
    }

    /**
     * Valida se uma string é não nula e não vazia.
     *
     * @param string a {@link java.lang.String} object.
     * @param message Mensagem a ser exibida em caso de erro de validação.
     * @return String validada sem nenhuma alteração.
     * @throws java.lang.NullPointerException Se string for null.
     * @throws java.lang.IllegalArgumentException Se string for vazia.
     */
    public static String notEmpty(final String string, final String message) {
        final String notNullString = notNull(string, message);

        if (notNullString.isEmpty()) {
            throwNewIllegalArgument(string, message);
        }

        return notNullString;
    }

    /**
     * Valida se uma coleção é não nula e não está vazia.
     *
     * @param collection Coleção.
     * @return Coleção validada sem nenhuma alteração.
     * @throws java.lang.NullPointerException Se coleção for null.
     * @throws java.lang.IllegalArgumentException Se coleção estiver vazia.
     */
    public static <T extends Collection<?>> T notEmpty(final T collection) {
        return notEmpty(collection, MSG_NOT_EMPTY);
    }

    /**
     * Valida se uma coleção é não nula e não está vazia.
     *
     * @param message Mensagem a ser exibida em caso de erro de validação.
     * @param collection Coleção.
     * @return Coleção validada sem nenhuma alteração.
     * @throws java.lang.NullPointerException Se coleção for null.
     * @throws java.lang.IllegalArgumentException Se coleção estiver vazia.
     */
    public static <T extends Collection<?>> T notEmpty(final T collection,
            final String message) {
        final T notNullCollection = notNull(collection, message);

        if (notNullCollection.isEmpty()) {
            throwNewIllegalArgument(collection, message);
        }

        return notNullCollection;
    }

    /**
     * Valida se uma coleção tem exatamente o tamanho (número de items)
     * desejado.
     *
     * @param collection Coleção a ser validada.
     * @param size Tamanho requerido para a coleção. Deve ser não negativo.
     * @return Coleção sem modificações ou null caso coleção seja null.
     * @throws java.lang.IllegalArgumentException Se coleção não for do tamanho
     * requerido ou se tamanho for negativo.
     */
    public static <T extends Collection<?>> T sizeRequired(final T collection,
            final int size) {
        return sizeRequired(collection, size, MSG_INVALID_SIZE);
    }

    /**
     * Valida se uma coleção tem exatamente o tamanho (número de items)
     * desejado.
     *
     * @param collection Coleção a ser validada.
     * @param size Tamanho requerido para a coleção. Deve ser não negativo.
     * @param message Mensagem a ser exibida em caso de erro de validação.
     * Parâmetros = {0}: tamanho atual, {1}: tamanho exigido.
     * @return Coleção sem modificações ou null caso coleção seja null.
     * @throws java.lang.IllegalArgumentException Se coleção não for do tamanho
     * requerido ou se tamanho for negativo.
     */
    public static <T extends Collection<?>> T sizeRequired(final T collection,
            final int size, final String message) {
        if (collection == null) {
            return null;
        }

        greaterThan(size, -1, "Size must be non negative.");

        if (collection.size() != size) {
            throwNewIllegalArgument(collection, message, collection.size(), size);
        }

        return collection;
    }

    /**
     * Valida se um array é não nulo e não vazio, ou seja, existe e contém pelo
     * menos um elemento.
     *
     * @param array Array
     * @return Array sem modificações.
     * @throws java.lang.NullPointerException Se array for null.
     * @throws java.lang.IllegalArgumentException Se array estiver vazio.
     */
    public static <T> T[] notEmpty(final Class<T> clasz, final T[] array) {
        return notEmpty(array, MSG_NOT_EMPTY);
    }

    /**
     * Valida se um array é não nulo e não vazio, ou seja, existe e contém pelo
     * menos um elemento.
     *
     * @param array Array
     * @param message Mensagem a ser exibida em caso de erro de validação.
     * @return Array sem modificações.
     * @throws java.lang.NullPointerException Se array for null.
     * @throws java.lang.IllegalArgumentException Se array estiver vazio.
     */
    public static <T> T[] notEmpty(final T[] array, final String message) {
        // ambiguidade de métodos garante que array não seja nulo;
        if (array.length < 1) {
            throwNewIllegalArgument(array, message);
        }

        return array;
    }

    /**
     * Valida se um array tem exatamente um tamanho (número de items) desejado.
     *
     * @param array Array a ser validado.
     * @param length Tamanho requerido para o array, deve ser não negativo.
     * @return Array sem modificações ou null caso array seja null.
     * @throws java.lang.IllegalArgumentException Se array não for do tamanho
     * requerido ou se o tamanho especificado é negativo.
     */
    public static <T> T[] lengthRequired(final T[] array, final int length) {
        return lengthRequired(array, length, MSG_INVALID_SIZE);
    }

    /**
     * Valida se um array tem exatamente um tamanho (número de items) desejado.
     *
     * @param array Array a ser validado.
     * @param length Tamanho requerido para o array, deve ser não negativo.
     * @param message Mensagem a ser exibida em caso de erro de validação.
     * Parâmetros = {0}: tamanho atual, {1}: tamanho exigido.
     * @return Array sem modificações ou null caso array seja null.
     * @throws java.lang.IllegalArgumentException Se array não for do tamanho
     * requerido ou se o tamanho especificado é negativo.
     */
    public static <T> T[] lengthRequired(final T[] array, final int length,
            final String message) {
        if (array == null) {
            return null;
        }

        greaterThan(length, -1, "Length must be non negative.");

        if (array.length != length) {
            throwNewIllegalArgument(array, message, array.length, length);
        }

        return array;
    }

    /**
     * Valida se um número está compreendido dentro de uma faixa de valores. Os
     * limites estão inclusos na faixa.
     *
     * @param number Número a ser validado.
     * @param lowerLimit Limite inferior da faixa.
     * @param upperLimit Limite superior da faixa.
     * @return Número validado sem nenhuma alteração ou null caso número seja
     * null.
     * @throws java.lang.NullPointerException Se algum parâmetro for null.
     * @throws java.lang.IllegalArgumentException Se número não estiver dentro
     * da faixa.
     */
    public static <T extends Number> T inRange(final T number,
            final T lowerLimit, final T upperLimit) {
        return inRange(number, lowerLimit, upperLimit, MSG_NOT_IN_RANGE);
    }

    /**
     * Valida se um número está compreendido dentro de uma faixa de valores. Os
     * limites estão inclusos na faixa.
     *
     * @param message Mensagem a ser exibida em caso de erro de validação.
     * Parâmetros = {0}: limite inferior, {1}: limite superior, {2}: valor
     * atual.
     * @param number Número a ser validado.
     * @param lowerLimit Limite inferior da faixa.
     * @param upperLimit Limite superior da faixa.
     * @return Número validado sem nenhuma alteração ou null caso número seja
     * null.
     * @throws java.lang.NullPointerException Se algum limite for null.
     * @throws java.lang.IllegalArgumentException Se número não estiver dentro
     * da faixa.
     */
    public static <T extends Number> T inRange(final T number,
            final T lowerLimit, final T upperLimit, final String message) {
        if (number == null) {
            return null;
        }

        notNull(lowerLimit, getParamMessage(MSG_NOT_NULL, "lowerLimit"));
        notNull(upperLimit, getParamMessage(MSG_NOT_NULL, "upperLimit"));

        if ((number.doubleValue() < lowerLimit.doubleValue())
                || (number.doubleValue() > upperLimit.doubleValue())) {
            throwNewIllegalArgument(number, message, lowerLimit, upperLimit,
                    number);
        }

        return number;
    }

    /**
     * Valida se um número é menor a um limite superior.
     *
     * @param number Número a ser validado.
     * @param upperLimit Limite superior.
     * @return Número validado sem nenhuma alteração ou null caso número seja
     * null.
     * @throws java.lang.NullPointerException Se algum limite for null.
     * @throws java.lang.IllegalArgumentException Se número maior que limite.
     */
    public static <T extends Number> T lowerThan(final T number,
            final T upperLimit) {
        return lowerThan(number, upperLimit, MSG_GREATER_THAN);
    }

    /**
     * Valida se um número é menor a um limite superior.
     *
     * @param message Mensagem a ser exibida em caso de erro de validação.
     * Parâmetros = {0}: limite superior, {1}: valor atual.
     * @param number Número a ser validado.
     * @param upperLimit Limite superior.
     * @return Número validado sem nenhuma alteração ou null caso número seja
     * null.
     * @throws java.lang.NullPointerException Se algum limite for null.
     * @throws java.lang.IllegalArgumentException Se número maior que limite.
     */
    public static <T extends Number> T lowerThan(final T number,
            final T upperLimit, final String message) {
        if (number == null) {
            return null;
        }

        notNull(upperLimit, getParamMessage(MSG_NOT_NULL, "upperLimit"));

        if (Numbers.compare(number, upperLimit) > -1) {
            throwNewIllegalArgument(number, message, upperLimit, number);
        }

        return number;
    }

    /**
     * Valida se um número é maior a um limite inferior.
     *
     * @param number Número a ser validado.
     * @param lowerLimit Limite inferior.
     * @return Número validado sem nenhuma alteração ou null caso número seja
     * null.
     * @throws java.lang.NullPointerException Se algum limite for null.
     * @throws java.lang.IllegalArgumentException Se número menor que limite.
     */
    public static <T extends Number> T greaterThan(final T number,
            final T lowerLimit) {
        return greaterThan(number, lowerLimit, MSG_LESS_THAN);
    }

    /**
     * Valida se um número é maior a um limite inferior.
     *
     * @param message Mensagem a ser exibida em caso de erro de validação.
     * Parâmetros = {0}: limite inferior, {1}: valor atual.
     * @param number Número a ser validado.
     * @param lowerLimit Limite inferior.
     * @return Número validado sem nenhuma alteração ou null caso número seja
     * null.
     * @throws java.lang.NullPointerException Se algum limite for null.
     * @throws java.lang.IllegalArgumentException Se número menor que limite.
     */
    public static <T extends Number> T greaterThan(final T number,
            final T lowerLimit, final String message) {
        if (number == null) {
            return null;
        }

        notNull(lowerLimit, getParamMessage(MSG_NOT_NULL, "lowerLimit"));

        if (Numbers.compare(number, lowerLimit) < 1) {
            throwNewIllegalArgument(number, message, lowerLimit, number);
        }

        return number;
    }

    /**
     * Realiza a validação de um objeto
     *
     * @param validatable Objeto validável.
     * @return Objeto válido ou null caso objeto seja null.
     */
    public static <T extends Validatable> T valid(final T validatable) {
        if (validatable == null) {
            return null;
        } else {
            RuleViolation violation = validatable.validate();

            if (violation != null) {
                throwNewIllegalArgument(validatable, MSG_INVALID,
                        violation.getMsg());
            }

            return validatable;
        }
    }

    /**
     * Realiza a validação de um objeto
     *
     * @param obj Objeto validável.
     * @param validator Validador do objeto.
     * @return Objeto válido ou null caso objeto seja null.
     * @throws NullPointerException Se validador for null.
     */
    public static <T> T valid(final T obj, final Validator<T> validator) {
        if (obj == null) {
            return null;
        } else {
            notNull(validator, getParamMessage(MSG_NOT_NULL, "validator"));

            RuleViolation violation = validator.validate(obj);

            if (violation != null) {
                throwNewIllegalArgument(obj, MSG_INVALID, violation.getMsg());
            }

            return obj;
        }
    }

    /**
     * Valida se um objeto pode ser convertido para um determinado tipo.
     *
     * @param type Tipo esperado do objeto.
     * @param obj Objeto a ser convertido para o tipo especificado.
     * @return Objeto convertido ou null se objeto for null.
     * @throws ClassCastException Se conversão não puder ser realizada.
     * @throws NullPointerException Se tipo for null.
     */
    public static <T> T instanceOf(final Class<T> type, final Object obj) {
        return instanceOf(type, obj, MSG_NOT_INSTANCE_OF);
    }

    /**
     * Valida se um objeto pode ser convertido para um determinado tipo.
     *
     * @param type Tipo esperado do objeto.
     * @param obj Objeto a ser convertido para o tipo especificado.
     * @param message Mensagem a ser exibida em caso de erro de validação.
     * Parâmetros = {0}: nome da classe do objeto, {1}: nome da classe do tipo.
     * @return Objeto convertido ou null se objeto for null.
     * @throws ClassCastException Se conversão não puder ser realizada.
     * @throws NullPointerException Se tipo for null.
     */
    public static <T> T instanceOf(final Class<T> type, final Object obj,
            final String message) {
        if (obj == null) {
            return null;
        }

        notNull(type, getParamMessage(MSG_NOT_NULL, "type"));

        if (!type.isAssignableFrom(obj.getClass())) {
            throw new ClassCastException(getErrorMsg(obj, message,
                    obj.getClass().getName(), type.getName()));
        }

        @SuppressWarnings("unchecked")
        final T t = (T) obj;

        return t;
    }

    /**
     * Valida se uma classe representa uma interface.
     *
     * @param objClass Referência a uma classe de interface.
     * @return Classe não modificada ou null caso classe seja null;
     * @throws IllegalArgumentException Se classe não representar uma interface.
     */
    public static <T> Class<T> interfaceRequired(final Class<T> objClass) {
        return interfaceRequired(objClass, MSG_NOT_AN_INTERFACE);
    }

    /**
     * Valida se uma classe representa uma interface.
     *
     * @param objClass Referência a uma classe de interface.
     * @param message Mensagem a ser exibida em caso de erro de validação.
     * @return Classe não modificada ou null caso classe seja null;
     * @throws IllegalArgumentException Se classe não representar uma interface.
     */
    public static <T> Class<T> interfaceRequired(final Class<T> objClass,
            final String message) {
        if (objClass == null) {
            return null;
        }

        if (!Modifier.isInterface(objClass.getModifiers())) {
            throwNewIllegalArgument(objClass, message);
        }

        return objClass;
    }

    /**
     * @param obj Objeto a ser retornado.
     * @param condition Condição a ser validada se é falsa.
     * @param message Mensagem para erro de validação.
     * @return Objeto sem alterações.
     */
    public static <T> T isFalse(final T obj, final boolean condition,
            final String message) {
        return isTrue(obj, !condition, message);
    }

    /**
     * @param obj Objeto a ser retornado.
     * @param condition Condição a ser validada se é verdadeira.
     * @param message Mensagem para erro de validação.
     * @return Objeto sem alterações.
     */
    public static <T> T isTrue(final T obj, final boolean condition,
            final String message) {
        if (!condition) {
            throwNewIllegalArgument(obj, message);
        }

        return obj;
    }

    /**
     * @param obj Objeto a ser verificado e retornado.
     * @param conditions Condições as quais o objeto deve seguir.
     * @return Objeto sem alterações.
     * @throws IllegalArgumentException Se objeto não satisfazer as condições.
     */
    public static <T> T check(final T obj, final Condition<T> conditions) {
        return check(obj, conditions, MSG_CHECK_FAIL);
    }

    /**
     * @param obj Objeto a ser verificado e retornado.
     * @param conditions Condições as quais o objeto deve seguir.
     * @param message Mensagem customizada. Recebe um parâmetro que é o toString da condição.
     * @return Objeto sem alterações.
     * @throws IllegalArgumentException Se objeto não satisfazer as condições.
     */
    public static <T> T check(final T obj, final Condition<T> conditions, final String message) {
        if (!conditions.check(obj)) {
            throwNewIllegalArgument(obj, message, conditions);
        }
        return obj;
    }

    /**
     * Lançador de exceções padrão.
     *
     * @param t Objeto validado.
     * @param message Mensagem de erro de validação.
     * @param params Parâmetros da mensagem.
     */
    private static <T> void throwNewIllegalArgument(final T t,
            final String message, final Object... params) {
        throw new IllegalArgumentException(getErrorMsg(t, message, params));
    }

    /**
     * @param t Objeto validado.
     * @param message Mensagem de erro de validação.
     * @param params Parâmetros da mensagem.
     * @return Mensagem de erro padrão.
     */
    static <T> String getErrorMsg(final T t, final String message,
            final Object... params) {
        String newMessage = "\n\nValidated object: " + t;
        newMessage += ("\nMessage: "
                + Strings.replaceParams(message, params) + "\n");

        return newMessage;
    }
}
